<script setup lang="tsx">
import { PopoverInstance, useFormItem } from 'element-plus'
import type {Component, ModelRef} from "vue";
import type {Node as TreeNode} from 'element-plus/es/components/tree/src/model/node'
import type {TreeNodeData} from "element-plus/es/components/tree/src/tree.type";
import {useVueFlow} from "@vue-flow/core";
let regExp = /\{\{([a-zA-Z0-9_]*?)\}\}/g;

defineOptions({
  name: 'expressionEditor'
})

const modelValue = defineModel<string>({default: ''}) as ModelRef<string>
if(modelValue.value == undefined){
  modelValue.value = ''
}
const props = defineProps<{
  treeData: Object[],
  maxlength: number,
  placeholder: string
}>()
const defaultMaxInputLength = 2000 // 默认最大输入长度
const emits = defineEmits(['blur'])
const editorRef = ref<HTMLElement>()

const {viewport} = useVueFlow()
const renderComponent = () => {
  return <div id="renderRoot" class="min-h-9 leading-9">{
    valueGrouping.value.map((segment, s) => {
      return (
          <div key={s} class="expression-editor-segment min-h-9 leading-9">
            {
              segment.map((str, t) => {
                let match = str.match(regExp)
                if(match) {
                  let prop = str.slice(2, -2)
                  let valid = checkValidProp(prop)
                  return <span key={t+'exp'} title={valid ? '': '变量不存在'} class={valid ? 'expression-is-valid expression-item' : 'expression-item expression-is-invalid'}>
                    {[ (s!=0 && t==0) ? '&#8203;' : '', str]}
                  </span>
                } else {
                  return <span key={t+'span'}>
                    {(s!=0 && t==0) ? <span key={t+'82'}>&#8203;{str}</span> : <span key={t+'83'}>{str}</span>}
                  </span>
                }
              })
            }
          </div>
      )
    })}
  </div>
}

const checkValidProp = (prop: string): boolean => {
  // console.log('check prop')
  if(props.treeData){
    return props.treeData.find(item => item.prop == prop) != undefined
  }else{
    return false
  }
}

// 数据分段
const splitSegment = computed<string[]>(() => {
  if(modelValue.value){
    return modelValue.value.split('\n')
  }else{
    return []
  }
})

// 字符串分组
const stringGrouping = (inputStr: string): string[] => {
  let result = [];
  let match;
  let lastIndex = 0;

  while ((match = regExp.exec(inputStr)) !== null) {
    // 添加匹配之前的部分
    result.push(inputStr.slice(lastIndex, match.index));
    // 添加匹配的部分（包括大括号）
    result.push(match[0]);
    lastIndex = match.index + match[0].length;
  }

  // 添加剩余的部分
  result.push(inputStr.slice(lastIndex));
  return result
}

const valueGrouping = computed<string[][]>(() => splitSegment.value.map(seg => stringGrouping(seg)))
// const valueGrouping = ref<string[][]>([])
// watch(() => splitSegment.value, (value, oldValue) => {
//   valueGrouping.value = value.map(seg => stringGrouping(seg))
// })

const elPopoverRef = ref<PopoverInstance>()
const cursorPosition = ref<[number, number]>([0, 0])

watch(cursorPosition, () => {
  nextTick(() => {
    update()
  })
})

// 根据光标位置
const update = () => {
  const editorDiv = editorRef.value as HTMLElement
  const nodeStart = getNodeAtNumChar(editorDiv, cursorPosition.value[0])

  if(nodeStart){
    const offsetStart = getNumCharBeforeNode(nodeStart, editorDiv)[0]
    // 这个nodeStart是当前文本节点，offsetStart是当前文本节点之前的字符数
    // 因此偏移数为：当前光标位置总字符数 减去 offsetStart
    setCursor(nodeStart, cursorPosition.value[0] - offsetStart)
  }
}

const setCursor = (node: Node, offset: number) => {
  document.getSelection()?.collapse(node, offset)
}

// 在指定位置插入文本
const insertText = (str: string, position: number) => {
  modelValue.value = modelValue.value.slice(0, position) + str + modelValue.value.slice(position)
}

// 删除指定位置的文本
const deleteText = (posStart: number, posEnd: number) => {
  modelValue.value = modelValue.value.slice(0, posStart) + modelValue.value.slice(posEnd)
}

// 获取节点在父节点中的字符位置
const getNumCharBeforeNode = (node: Node, parent: Node): [number, boolean] => {
  let n: number = 0
  let found: boolean = false
  parent.childNodes.forEach(childNode => {
    if (childNode === node) {
      found = true
    }
    if (!found) {
      if (childNode.hasChildNodes()) {
        const [nChild, foundChild] = getNumCharBeforeNode(node, childNode)
        n += nChild
        found = foundChild
      } else if (childNode.nodeType !== Node.COMMENT_NODE) {
        n += childNode.textContent?.length ?? 0
      }
    }
  })
  return [n, found]
}

const getNodeAtNumChar = (parent: Node, num: number): Node|null => {
  let n = 0
  let found = false
  let node: ChildNode | null = null
  for(const childNode of parent.childNodes){
    if (!found) {
      if (childNode.nodeType !== Node.COMMENT_NODE) {
        n += childNode.textContent?.length ?? 0
        if (n >= num) {
          found = true
          node = childNode
        }
      }
    }
  }
  if (node && node.hasChildNodes()) {
    return getNodeAtNumChar(node, num - (n - (node.textContent?.length ?? 0)))
  }
  return node
}

const isComposition = ref<boolean>(false)
const handleCompositionStart = (event: InputEvent) => {
  isComposition.value = true
}

const handleCompositionEnd = (event: InputEvent) => {
  setTimeout(() => {
    isComposition.value = false
  })
  let text = event.data
  if(text){
    if(modelValue.value.length + text.length > (props.maxlength || defaultMaxInputLength)){
      return
    }
    const editorDiv = editorRef.value as HTMLElement
    const range = window.getSelection()?.getRangeAt(0)

    if(range){
      let copyValue = range.startContainer.nodeValue || ''
      let cleanVal = copyValue.replace(/\u200B/g, '');
      if(copyValue != cleanVal){
        // 第二行及以后的行，处理每行第一个包含零宽度空格的特殊span
        // 因为有零宽度空格，重写dom之后，range的startOffset会被重置为0
        // 因此需要重新设置startOffset
        range.startContainer.nodeValue = '\u200B'
        const nCharBefore1 = getNumCharBeforeNode(range.startContainer, editorDiv)[0]
        insertText(text, nCharBefore1 + 1)
        let newOffset = nCharBefore1 + 1 + text.length
        cursorPosition.value = [newOffset, newOffset]
      }else{
        const nCharBefore1 = getNumCharBeforeNode(range.startContainer, editorDiv)[0]
        // 1. 判断当前中文输入是在变量节点span中，根据父元素的class判断，包含'expression-item'的span为变量节点
        // 这里也有两种情况，一种是在变量节点中输入中文，另外一种是变量节点中输入英文，暂时两种情况没区别
        // 都统一去掉dom中的当前输入内容，然后更新modelValue,由数据变化去驱动dom更新
        let textNodeValue = range.startContainer.nodeValue || ''
        let match = textNodeValue?.match(/\{\{/)
        let parentClass = range.startContainer.parentElement?.className
        if(match && parentClass?.includes('expression-item')){
          let beforeStartOffset = range.startOffset
          let beforeEndOffset = range.endOffset
          // 去除dom中，当前输入的内容，不管是中文还是英文，恢复到输入之前的内容
          const beforeInputStartOffset = beforeStartOffset - text.length // 应该大于0
          range.startContainer.nodeValue = textNodeValue.slice(0, beforeInputStartOffset) + textNodeValue.slice(beforeStartOffset)
          insertText(text, nCharBefore1 + beforeInputStartOffset)
          let newOffset = nCharBefore1 + beforeInputStartOffset + text.length
          cursorPosition.value = [newOffset, newOffset]
        }else{
          // 2. 不在变量节点中输入中文,即在常规span节点中输入中文
          let parentId = range.startContainer.parentElement?.id
          const startOffset = range.startOffset
          if(parentId && parentId == 'renderRoot'){
            (range.startContainer as HTMLElement).remove()
          }
          insertText(text, nCharBefore1 + startOffset - (text.length || 0))
          let newOffset = nCharBefore1 + startOffset
          cursorPosition.value = [newOffset, newOffset]
        }
      }
    }
  }
}

const { formItem } = useFormItem()
const handleBlur = (event: FocusEvent) => {
  emits('blur', modelValue.value)
  formItem?.validate('blur').catch((err) => {})
}
watch(modelValue, (val) => {
  formItem?.validate('change').catch((err) => {})
})

const handleMouseWheel = (event: WheelEvent) => {
  event.stopPropagation()
}

const handleBeforeInput = (event: InputEvent) => {
  event.preventDefault()
  event.stopPropagation()
  if (
      [
        'insertText', // 英文输入
        'insertParagraph', // 回车
        'insertLineBreak',
        'deleteContentBackward', // 删除
        'deleteContentForward',
        'insertFromPaste', // 粘贴
      ].includes(event.inputType)
  ) {
    const editorDiv = editorRef.value as HTMLElement
    const [range] = event.getTargetRanges()
    const nCharBefore1 = getNumCharBeforeNode(range.startContainer, editorDiv)[0]
    if(event.inputType === 'insertText'){
      if(modelValue.value.length + (event.data?.length || 0) > (props.maxlength || defaultMaxInputLength)){
        return
      }
      // 在光标位置插入文本
      let spaceOffset = 0
      // 如果光标在换行符位置（即零宽度空格），则插入文本需要往后移动1位
      if(range.startContainer?.nodeValue?.match('\u200B')) spaceOffset = 1
      insertText(event.data ?? '', nCharBefore1 + range.startOffset + spaceOffset)
      let newOffset = nCharBefore1 + range.startOffset + (event.data?.length ?? 0) + spaceOffset
      cursorPosition.value = [newOffset, newOffset]
    }else if(event.inputType === 'insertCompositionText'){
      // 中文输入，另行处理
    } else if(event.inputType === 'insertParagraph' || event.inputType === 'insertLineBreak'){
      // 在光标位置插入换行符
      if(modelValue.value.length + 1 > (props.maxlength || defaultMaxInputLength)){
        return
      }
      insertText('\n', nCharBefore1 + range.startOffset)
      let newOffset = nCharBefore1 + range.startOffset + 1
      cursorPosition.value = [newOffset, newOffset]
    }else if(event.inputType === 'insertFromPaste'){
      const data = event.dataTransfer?.getData('text/plain')
      if(data){
        if(modelValue.value.length + data?.length > (props.maxlength || defaultMaxInputLength)){
          return
        }
        insertText(data, nCharBefore1 + range.startOffset)
        let newOffset = nCharBefore1 + range.startOffset + data.length
        cursorPosition.value = [newOffset, newOffset]
      }
    } else if(['deleteContentBackward', 'deleteContentForward'].includes(event.inputType)){
      const nCharBefore2 = getNumCharBeforeNode(range.endContainer, editorDiv)[0]
      deleteText(nCharBefore1 + range.startOffset, nCharBefore2 + range.endOffset)
      let newOffset = nCharBefore1 + range.startOffset
      cursorPosition.value = [newOffset, newOffset]
    }
  }else{
    // 其他输入类型，不处理

  }
}

const expressionSuggestionRef = ref<HTMLElement>()
const expressionContainerRef = ref<HTMLElement>()
const expressPosition = ref<[number, number]>([0,0])
const getExpressPosition = (event: MouseEvent) => {
  const editorDiv = editorRef.value as HTMLElement
  const range = window.getSelection()?.getRangeAt(0)
  if(range){
    const nCharBefore1 = getNumCharBeforeNode(range.startContainer, editorDiv)[0]
    let afterStr = modelValue.value.slice(nCharBefore1)
    const regex = /}}/
    const match = regex.exec(afterStr)
    if(match){
      let nodeOffset = match.index + 2
      expressPosition.value = [nCharBefore1, nCharBefore1 + nodeOffset]
    }
  }
}

const isDragging = ref(false)
const startX = ref(0)
const startY = ref(0)
const suggestionPostion = ref<[string, string]>(['0px', '0px'])
const handleMouseDown = (event: MouseEvent) => {
  isDragging.value = false
  startX.value = event.clientX
  startY.value = event.clientY

  const handleMouseMove = (event: MouseEvent) => {
    if (Math.abs(event.clientX - startX.value) > 5 || Math.abs(event.clientY - startY.value) > 5) {
      isDragging.value = true
    }
  }

  const handleMouseUp = (e: MouseEvent) => {
    const target = e.target
    if (!isDragging.value) {
      // console.log('Clicked')
      if(target instanceof HTMLElement && target.classList.contains('expression-item')){
        const spanRect = target.getBoundingClientRect()
        const divRect = (expressionContainerRef.value as HTMLElement).getBoundingClientRect()
        const relativeX = spanRect.left / viewport.value.zoom - divRect.left / viewport.value.zoom + (spanRect.width / 2 / viewport.value.zoom)
        const relativeY = spanRect.top / viewport.value.zoom - divRect.top / viewport.value.zoom + (spanRect.height / 5 / viewport.value.zoom)
        suggestionPostion.value = [relativeX + 'px', relativeY + 'px']
        expressionSuggestionRef.value?.click()
        getExpressPosition(e)
      }
    } else {
      console.log('Dragged and selected text')
    }

    window.removeEventListener('mousemove', handleMouseMove)
    window.removeEventListener('mouseup', handleMouseUp)
  }

  window.addEventListener('mousemove', handleMouseMove)
  window.addEventListener('mouseup', handleMouseUp)
}

watch(() => viewport.value.zoom, () => {
  if(elPopoverRef.value){
    elPopoverRef.value.hide()
  }
})

useEventListener(expressionContainerRef, 'mousedown', handleMouseDown)

// 光标位置插入和替换表达式
const insertExpress = (express: string) => {
  const [left, right] = expressPosition.value
  if(right){
    const str = modelValue.value.slice(0, left) + `{{${express}}}` + modelValue.value.slice(right)
    modelValue.value = str
  }
}


// 树形选项逻辑
const currentExpression = ref<string>('')
const currentExpressionValuePath = computed<string[]>(() => currentExpression.value && currentExpression.value.split('.') || [])

const handleNodeClick = (nodeData: TreeNodeData, node: TreeNode) => {
  const valPath = []
  let currentNode = node
  while(currentNode) {
    valPath.unshift(currentNode.label)
    currentNode = currentNode.parent
  }
  // 删除数组中多余的undefined
  valPath.shift()
  currentExpression.value = valPath.join('.')
  insertExpress(currentExpression.value)
}

const activeNodeClass = (data: TreeNodeData, node: TreeNode) => {
  if(!currentExpression.value) return ''
  if(node.level == (currentExpressionValuePath.value.length - 1) && data.label == currentExpressionValuePath.value.slice(-1)[0]){
    return 'active-node'
  }else{
    return ''
  }
}


</script>

<template>
  <div ref="expressionContainerRef" class="expression-editor-container bg-white w-full rounded-md" >
    <div class="expression-editor-render editor-render-minRows-4">
      <div ref="editorRef"
           class="min-h-[100px] max-h-[270px] overflow-y-auto expression-editor-raw"
           contenteditable="true"
           @mousewheel.stop="handleMouseWheel"
           @blur="handleBlur"
           @compositionstart="handleCompositionStart"
           @compositionend="handleCompositionEnd"
           @beforeinput.stop.prevent="handleBeforeInput">
        <Component :is="renderComponent"></Component>
      </div>
    </div>

    <span v-if="!valueGrouping.length && !isComposition" class="expression-editor-placeholder">{{placeholder || '请输入'}}</span>

    <client-only>
      <el-popover
          placement="top"
          ref="elPopoverRef"
          trigger="click"
          :teleported="true"
      >
        <template #reference>
          <div :style="{left: suggestionPostion[0], top: suggestionPostion[1]}" ref="expressionSuggestionRef" class="expression-editor-suggestion"></div>
        </template>
        <template #default>
          <el-tree
              @node-click="handleNodeClick"
              :data="treeData || []"
              :props="{ class: activeNodeClass, label: 'prop' }"
          ></el-tree>
        </template>
      </el-popover>
    </client-only>
  </div>
</template>

<style lang="scss">
.expression-editor-raw{
  color: var(--el-text-color-regular);
  padding: 2px 10px;
  caret-color: var(--el-text-color-regular);
  cursor: text;
}
.expression-is-valid{
  color: var(--el-color-primary);
}
.expression-is-invalid{
  color: var(--el-color-danger);
}
.expression-item{
  font-weight: bold;
  display: inline-block;
  padding: 0 3px;
}
.expression-editor-container{
  position: relative;
}
.expression-editor-container:focus-within{
  box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
.el-form-item.is-error .expression-editor-container{
  box-shadow: 0 0 0 1px var(--el-color-danger) inset;
}
.expression-editor-placeholder {
  position: absolute;
  top: 0;
  left: 0;
  color: var(--el-text-color-placeholder);
  pointer-events: none;
  transition: all 0.3s;
  user-select: none;
  padding: 2px 10px;
}
.expression-editor-raw span{
  white-space: pre-wrap;
  word-break: break-all;
}
.expression-editor-suggestion{
  position: absolute;
}
</style>
